5.01. Рекомендации по разработке на JavaScript
Рекомендации по разработке на JavaScript
Так, представим, что вы уже изучили основы языка. Как правильно разрабатывать на C#?
Общие принципы
Чистый код и качество
Качество кода определяется количеством вопросов, которые возникают при его чтении. Чем меньше вопросов, тем выше качество. Основная цель разработки — создание понятного, поддерживаемого и расширяемого кода.
Код должен быть читаемым для других разработчиков. Человеческая память ограничена, поэтому методы должны быть короткими и понятными. Идеальный метод содержит не более семи входящих параметров.
Один оператор на строку
Каждый оператор заканчивается точкой с запятой. Автоматическая точка с запятой не используется. Это правило обеспечивает предсказуемость выполнения кода и избегает неожиданных ошибок.
Использование современных возможностей языка
Приоритет отдается современным возможностям JavaScript. Код пишется с использованием актуальных версий языка и стандартов. Устаревшие конструкции заменяются современными аналогами.
Декларативный подход
Декларативный стиль программирования предпочтительнее императивного, где это упрощает понимание. Декларативный код описывает, что нужно сделать, а не как это сделать.
Требования к именованию
Стили именования
Различные элементы кода используют разные стили именования:
- Переменные, параметры, функции, методы, свойства —
camelCase - Классы, конструкторы, интерфейсы —
PascalCase - Константы —
UPPER_CASEилиcamelCaseвнутри функций - Обработчики событий — префикс
onили суффиксHandler
Названия переменных
Имена переменных передают их сущность и роль в программе. Переменные называются осмысленно, чтобы любой разработчик мог сразу понять их назначение.
Значения именуются существительными: user, product, config. Массивы именуются во множественном числе: users, products, items. Функции именуются глаголами или глаголами с существительными: getUser, updateProduct, validateForm.
Названия функций и методов
Функции и методы именуются глаголами, которые описывают выполняемое действие. Имена должны быть описательными и передавать суть функции. Аргументы функций также именуются осмысленно.
Примеры корректных имен:
function notifyUser(emailAddress) { }
function calculateTotalPrice(items) { }
function validateFormData(data) { }
Избегание избыточности
Имена не содержат избыточных сведений о контексте. Префиксы и суффиксы используются только при необходимости. Названия должны быть краткими, но понятными.
Плохо:
const userUserName = "John";
const productProductPrice = 100;
Хорошо:
const userName = "John";
const productPrice = 100;
Константы и магические числа
Магические числа и строки выносятся в константы. Литеральные значения не используются в коде, кроме случаев, когда их смысл ясен из контекста.
const MAX_USERS = 100;
const DEFAULT_TIMEOUT = 5000;
const API_BASE_URL = "https://api.example.com";
Требования по оформлению
Отступы и пробелы
Отступы составляют четыре пробела. Табуляция не используется. Операторы окружены пробелами. После запятых ставится пробел.
let x = a + b;
let items = ["item1", "item2", "item3"];
Фигурные скобки
Фигурные скобки используются для всех управляющих структур, даже если тело содержит один оператор. Открывающая скобка находится на той же строке, что и оператор. Закрывающая скобка — на новой строке.
if (condition) {
statements;
} else if (anotherCondition) {
statements;
} else {
statements;
}
for (let i = 0; i < items.length; i++) {
statements;
}
while (condition) {
statements;
}
Длина строк
Строки ограничиваются 65 символами для повышения читаемости. Длинные инструкции разбиваются на несколько строк. Точки переноса размещаются перед операторами.
Пустые строки
Пустые строки разделяют логические блоки кода:
- Между методами класса
- Между многострочными выражениями
- После закрытия парных скобок
- Между несвязанными блоками кода
Кавычки
Для строк используются одинарные кавычки. Двойные кавычки применяются в HTML-атрибутах. Шаблонные литералы оформляются обратными кавычками.
const message = 'Hello, world!';
const html = "<div class='container'></div>";
const greeting = `Hello, ${name}!`;
Объектные литералы
Объектные литералы представляют собой либо структуры с ключами без кавычек, либо словари с ключами в кавычках. Эти типы ключей не смешиваются в одном объекте.
// Структура
const config = {
width: 42,
height: 100,
enabled: true
};
// Словарь
const translations = {
'hello': 'Привет',
'goodbye': 'До свидания'
};
Требования к переменным
Объявление переменных
Все локальные переменные объявляются с помощью const или let. Ключевое слово const используется по умолчанию. let применяется, когда переменная нуждается в переназначении. Ключевое слово var не используется.
const PI = 3.14159;
const users = [];
let counter = 0;
counter = counter + 1;
Одна переменная на объявление
Каждая декларация объявляет только одну переменную. Совместное объявление нескольких переменных не используется.
// Плохо
let a = 1, b = 2, c = 3;
// Хорошо
const a = 1;
const b = 2;
const c = 3;
Деструктуризация
Деструктуризация используется для извлечения значений из объектов и массивов. Это упрощает доступ к вложенным свойствам и делает код чище.
const { inputIndex, readOnly, elementConfig: { linkedCollectionName, linkedCollectionGuid } } = this.props;
const [first, second, third] = items;
Неиспользуемый код
Неиспользуемый код не остается в программе в закомментированном виде. Такой код удаляется. При необходимости его можно найти в системе контроля версий.
Отладочные команды
Команда debugger не остается в итоговом коде. Отладочные точки удаляются перед коммитом изменений.
Требования к функциям
Размер и сложность функций
Функции должны решать одну задачу. Идеальная функция содержит не более двух аргументов. Функции с большим количеством параметров декомпозируются или принимают объект с настройками.
// Плохо
function getUsers(fields, fromDate, toDate, sortBy, limit) { }
// Хорошо
function getUsers({ fields, fromDate, toDate, sortBy, limit }) { }
Аргументы по умолчанию
Аргументы по умолчанию предпочтительнее условных конструкций внутри функции. Это делает сигнатуру функции более понятной.
// Плохо
function createShape(type) {
const shapeType = type || "cube";
}
// Хорошо
function createShape(type = "cube") { }
Возврат значений
Функции возвращают значения явно. Неявное возвращение используется только для коротких стрелочных функций. Функции не возвращают null для коллекций — вместо этого возвращается пустой массив или объект.
function getItems() {
if (!hasItems) {
return []; // Вместо null
}
return items;
}
Чистые функции
Предпочтение отдается чистым функциям — функциям без побочных эффектов. Чистые функции проще тестировать и понимать. Они всегда возвращают одинаковый результат для одинаковых входных данных.
Стрелочные функции
Стрелочные функции используются для коротких колбэков и обработчиков. Для методов класса и конструкторов используются обычные функции.
const doubled = numbers.map(num => num * 2);
const filtered = items.filter(item => item.active);
Функции высшего порядка
Функции высшего порядка предпочтительнее традиционных циклов. Методы map, filter, reduce, find, some, every делают код более декларативным.
// Плохо
let sum = 0;
for (let num of numbers) {
sum += num;
}
// Хорошо
const sum = numbers.reduce((total, num) => total + num, 0);
Требования к массивам
Создание массивов
Для создания массивов используется литеральная нотация. Конструктор Array не применяется.
const items = [];
const users = ["John", "Jane", "Bob"];
Добавление элементов
Для добавления элементов в массив используется метод push. Прямое присваивание по индексу не применяется.
items.push("newItem");
Копирование массивов
Для копирования массивов используется оператор расширения .... Для глубокого копирования применяется комбинация JSON.parse и JSON.stringify или специализированные библиотеки.
const itemsCopy = [...items];
const deepCopy = JSON.parse(JSON.stringify(items));
Итерация по массивам
Итерация по массивам выполняется с помощью методов высшего порядка. Циклы for-in и for-of используются только в специальных случаях.
items.forEach(item => {
console.log(item);
});
const activeItems = items.filter(item => item.active);
const names = items.map(item => item.name);
Требования к объектам
Создание объектов
Для создания объектов используется литеральная нотация. Конструктор Object не применяется.
const user = {
name: "John",
age: 30,
email: "john@example.com"
};
Копирование объектов
Для копирования объектов используется метод Object.assign или оператор расширения. Глубокое копирование выполняется рекурсивно или с помощью специализированных библиотек.
const userCopy = { ...user };
const merged = { ...defaults, ...config };
Установка свойств по умолчанию
Свойства объектов по умолчанию устанавливаются с помощью Object.assign. Это делает код более читаемым и предсказуемым.
function createShape(config) {
config = Object.assign({
type: "cube",
width: 250,
height: 250
}, config);
}
Доступ к свойствам
Для доступа к свойствам объекта используется точечная нотация. Квадратные скобки применяются только для динамических ключей.
const name = user.name;
const key = "email";
const value = user[key];
Требования к классам
Объявление классов
Классы объявляются с помощью ключевого слова class. Функциональные конструкторы не используются в современном коде.
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, ${this.name}!`;
}
}
Наследование
Наследование реализуется с помощью ключевого слова extends. Множественное наследование не поддерживается — для этого используются миксины или композиция.
class Admin extends User {
constructor(name, email, permissions) {
super(name, email);
this.permissions = permissions;
}
}
Статические методы
Статические методы объявляются с помощью ключевого слова static. Они вызываются на классе, а не на экземпляре.
class MathUtils {
static add(a, b) {
return a + b;
}
}
const result = MathUtils.add(2, 3);
Приватные поля
Приватные поля и методы объявляются с префиксом #. Это обеспечивает инкапсуляцию и защиту внутреннего состояния.
class Counter {
#count = 0;
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
Требования к асинхронному программированию
Промисы
Асинхронные операции реализуются с помощью промисов. Коллбэки используются только в специальных случаях.
function fetchData(url) {
return fetch(url)
.then(response => response.json())
.then(data => data)
.catch(error => {
console.error("Error:", error);
throw error;
});
}
Async/Await
Синтаксис async/await предпочтительнее цепочек промисов. Он делает асинхронный код более читаемым и похожим на синхронный.
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to fetch user data:", error);
throw error;
}
}
Обработка ошибок
Ошибки в асинхронном коде обрабатываются с помощью try/catch. Неперехваченные ошибки логируются. Промисы всегда завершаются обработчиком ошибок.
async function loadData() {
try {
const data = await fetchData();
process(data);
} catch (error) {
logger.error("Failed to load data:", error);
showErrorMessage("Не удалось загрузить данные");
}
}
Требования к обработке ошибок
Строгие сравнения
Операторы === и !== используются вместо == и !=. Это предотвращает неожиданное приведение типов.
if (value === 5) { }
if (name !== null) { }
Проверка аргументов
Аргументы функций проверяются на корректность. Проверка включает тип, диапазон значений и формат. Исключения выбрасываются для некорректных аргументов.
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Arguments must be numbers");
}
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
Исключения для исключительных ситуаций
Исключения используются только для исключительных ситуаций. Управление потоком выполнения не осуществляется с помощью исключений. Проверки предпочтительнее перехвата исключений.
// Плохо
try {
const value = obj.property.value;
} catch (error) {
// Обработка ошибки
}
// Хорошо
if (obj && obj.property) {
const value = obj.property.value;
}
Логирование ошибок
Ошибки логируются с полной информацией. Используется метод toString() для получения стека вызовов и внутренних исключений.
try {
// Код
} catch (error) {
logger.error(`Operation failed: ${error.toString()}`);
// Или
logger.error(`Operation failed: ${error}`);
}
Требования к комментариям
Общие правила
Комментарии добавляются для документирования кода. Комментарии размещаются на отдельной строке, а не в конце строки кода. Текст комментария начинается с заглавной буквы и заканчивается точкой.
// Получение данных пользователя
const userData = getUserData(userId);
// Валидация входных параметров
validateInput(params);
JSDoc
Для документирования функций, классов и методов используется формат JSDoc. Комментарии содержат описание, параметры, возвращаемые значения и возможные исключения.
/**
* Создает нового пользователя
* @param {string} name - Имя пользователя
* @param {string} email - Email пользователя
* @param {number} age - Возраст пользователя
* @returns {User} Созданный пользователь
* @throws {Error} Если email уже существует
*/
function createUser(name, email, age) {
// Реализация
}
Блочные комментарии
Блочные комментарии снабжены отступом на том же уровне, что и окружающий код. Многострочные комментарии начинаются с /* и заканчиваются */. Последующие строки начинаются с *, выровненного с предыдущей строкой.
/*
* Это многострочный
* комментарий
* с правильным форматированием
*/
Избегание TODO-комментариев
Комментарии для отслеживания работы, которая должна быть сделана позже, не используются. Вместо этого применяется система трекинга задач. Закомментированный код удаляется.
Требования к работе с данными
Валидация входных данных
Все значения, поступающие извне, проходят валидацию. Валидация включает проверку типа, формата, диапазона и бизнес-правил.
function processUserData(userData) {
if (!userData || typeof userData !== "object") {
throw new Error("Invalid user data");
}
if (!userData.email || !isValidEmail(userData.email)) {
throw new Error("Invalid email format");
}
// Обработка данных
}
Параметризованные запросы
При работе с базами данных используются параметризованные запросы. Это предотвращает SQL-инъекции и другие уязвимости.
// Плохо
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Хорошо
const query = "SELECT * FROM users WHERE id = ?";
const result = db.query(query, [userId]);
Ограничение запрашиваемых данных
Запрашиваются только те данные, которые нужны для работы. Полные таблицы не загружаются без необходимости. Используются пагинация и фильтрация на стороне сервера.
// Плохо
const allUsers = await db.query("SELECT * FROM users");
// Хорошо
const users = await db.query(
"SELECT id, name, email FROM users WHERE active = 1 LIMIT 100"
);
Требования к производительности
Избегание частых аллокаций
Частые аллокации объектов в критических участках кода избегаются. Объекты создаются за пределами циклов. Повторное использование объектов предпочтительнее создания новых.
// Плохо
for (let i = 0; i < items.length; i++) {
const temp = { value: items[i] };
process(temp);
}
// Хорошее
const temp = {};
for (let i = 0; i < items.length; i++) {
temp.value = items[i];
process(temp);
}
Оптимизация циклов
Циклы оптимизируются для повышения производительности. Длина массива кэшируется. Вложенные циклы минимизируются.
for (let i = 0, len = items.length; i < len; i++) {
// Обработка
}
Отложенные вычисления
Тяжелые вычисления выполняются по требованию. Кэширование результатов предотвращает повторные вычисления.
class DataProcessor {
#cachedResult = null;
getResult() {
if (this.#cachedResult === null) {
this.#cachedResult = this.#calculateExpensiveResult();
}
return this.#cachedResult;
}
}
Требования к безопасности
Экранирование данных
Данные экранируются перед выводом в HTML. Это предотвращает XSS-атаки. Специальные символы заменяются соответствующими HTML-сущностями.
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
Валидация на стороне сервера
Все данные валидируются на стороне сервера. Клиентская валидация дополняет, но не заменяет серверную.
Защита конфиденциальных данных
Конфиденциальные данные не выводятся в логи и ошибки. API-ключи, пароли и персональные данные защищаются.
// Плохо
logger.error(`Failed to authenticate user ${username} with password ${password}`);
// Хорошо
logger.error(`Failed to authenticate user ${username}`);
Требования к тестированию
Покрытие тестами
Критически важные компоненты покрываются модульными и интеграционными тестами. Тесты пишутся до или одновременно с реализацией (TDD/BDD).
Изоляция тестов
Тесты изолированы друг от друга. Каждый тест может выполняться независимо. Состояние между тестами не сохраняется.
Описательные названия тестов
Названия тестов описывают проверяемое поведение. Тесты читаются как спецификация.
describe("UserService", () => {
it("should create a new user with valid data", () => {
// Тест
});
it("should throw an error when email is invalid", () => {
// Тест
});
});
Инструменты и настройки
ESLint
ESLint настраивается для автоматической проверки кода. Правила включают проверку стиля, потенциальных ошибок и лучших практик.
Prettier
Prettier используется для автоматического форматирования кода. Настройки включают отступы, кавычки, точку с запятой и длину строк.
EditorConfig
Файл .editorconfig обеспечивает единообразие настроек редактора между разработчиками. Настройки включают отступы, кодировку и окончания строк.
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
Git Hooks
Git hooks используются для автоматической проверки кода перед коммитом. Pre-commit хуки запускают линтер и тесты.